import java.lang.reflect.Array;


/**
 * This class represents a HashTable that will hold mutiple HashNodes
 * @author Matt
 *
 * @param <K>
 * @param <V>
 */
public class HashTable<K,V> {

	
	private HashNode<K,V>[] table;
	private double maxLoad;
	private HashFunction<K> hashFunc;
	private int size;

	@SuppressWarnings("unchecked")
	public HashTable(int tableSize, double maxLoad, HashFunction<K> func)
	{
		table = (HashNode<K,V>[])new HashNode[tableSize];
		this.maxLoad = maxLoad;
		hashFunc = func;
		this.size = 0;
	}
	
	@SuppressWarnings("unchecked")
	public HashTable()
	{
		this(89, .7, (HashFunction<K>) new JavaHash());
	}
	
	public HashTable(HashFunction<K> func)
	{
		this(89, .7, func);
	}
	/**
	 * This method will add a new HashNode in to the table at the location specified by the hash Function 
	 * @param key New key value
	 * @param value New value
	 */
	public void add(K key, V value)
	{
		if((size/table.length) >= maxLoad)
		{
			regrow();
		}
		boolean sameKey = false;
		HashNode<K,V> newNode = new HashNode<K,V>(key, value);
		int index = hashFunc.hash(key)% table.length;
		HashNode<K,V> curNode = table[index];

		while(curNode!=null && !sameKey)
		{
			if(curNode.getKey().equals(key))
				sameKey = true;
			else
				curNode = curNode.getNext();
			
		}
		if(!sameKey)
		{
			if(table[index] == null)
			{
				table[index] = newNode;
			}
			else
			{
				HashNode<K,V> probe = table[index];
				while(probe.getNext() != null)
				{
					probe = probe.getNext();
				}
				probe.setNext(newNode);
			}
			size++;
		}
		
	}
	/**
	 * Regrow will double the size of the table and rehash every value 
	 * 
	 */
	@SuppressWarnings("unchecked")
	public void regrow()
	{
		HashNode<K,V>[] oldTable = table;
		int oldLength = table.length;
		table = (HashNode<K,V>[])new HashNode[(oldLength*2)+1];
		size = 0;
		HashNode<K,V> curNode = null;
		for(int i = 0; i < oldLength; i++)
		{
			curNode = oldTable[i];
			if(curNode != null && curNode.getNext() == null)
			{
				add(curNode.getKey(), curNode.getValue());
			}
			else
			{
				while(curNode != null)
				{
					add(curNode.getKey(), curNode.getValue());
					curNode = curNode.getNext();
				}
			}
		}
	}
	/**
	 * This method will calculate the number of collisions in the table
	 * @return Number of collision
	 */
	public int collisionCount()
	{
		int count = 0;
		HashNode<K,V> curNode = null;
		for(int i = 0; i<table.length; i++)
		{
			curNode = table[i];
			while(curNode != null && curNode.getNext()!=null)
			{
				count++;
				curNode = curNode.getNext();
			}
		}
		return count;
	}
	/**
	 * Ts method will create a new table to replace the current table.
	 */
	public void clear()
	{
		@SuppressWarnings("unchecked")
		HashNode<K, V>[] newTable = (HashNode<K,V>[])new HashNode[table.length];
		size = 0;
		table = newTable;
	}
	/**
	 * This will check for a node in the table that has the same key as was passed in
	 * @param key Target Key
	 * @return boolean
	 */
	public boolean contains(K key)
	{
		HashNode<K,V> curNode; 
		boolean found = false;
		int index = hashFunc.hash(key)% table.length;
		curNode = table[index];
				if(curNode != null && curNode.getNext() == null)
				{
					if(curNode.getKey().equals(key))
						found = true;
				}
				else
				{
					while(curNode != null && !found)
					{
						if(curNode.getKey().equals(key))
							found = true;
						curNode = curNode.getNext();
					}
				}
			
		
		return found;
	}
	/**
	 * This method will return the value of the Node with the passes in Key
	 * @param key Target Key
	 * @return V value of the node with the key
	 */
	public V get(K key)
	{
		V value = null;
		HashNode<K,V> curNode; 
		boolean found = false;
		int index = hashFunc.hash(key)% table.length;
		curNode = table[index];
		if(curNode != null)
		{
				if(curNode != null && curNode.getNext() == null)
				{
					if(curNode.getKey().equals(key))
					{
						found = true;
						value = curNode.getValue();
					}
				}
					
				else
				{
					while(curNode != null && !found)
					{
						if(curNode.getKey().equals(key))
						{
							found = true;
							value = curNode.getValue();
						}
						curNode = curNode.getNext();
					}
				}
		}
		
		return value;
	}
	/**
	 * This method will remove the Node with the Key specified
	 * @param key Target
	 * @return Value of the removed Node
	 */
	public V remove(K key)
	{

		V value = null;
		HashNode<K,V> curNode; 

		int index = hashFunc.hash(key)% table.length;
		curNode = table[index];


		if(curNode!=null)
		{
			if(curNode.getNext() == null)
			{
				if(curNode.getKey().equals(key))
				{
					table[index] = curNode.getNext();
					curNode.setNext(null);
					value = curNode.getValue();
				}
			}
			else
			{
				while(curNode.getNext()!=null)
				{
					if(curNode.getNext().getKey().equals(key))
						{
							curNode.setNext(curNode.getNext().getNext());
							value = curNode.getValue();
						}
					else if(curNode.getKey().equals(key))
					{
						table[index] = curNode.getNext();
						curNode.setNext(null);
						value = curNode.getValue();
					}
					else
					{
						curNode = curNode.getNext();
					}
			}
			}
			size--;
		}
		return value;
	}
	
	/**
	 * ****************************************************************************************
	 * DO NOT CHANGE THIS TOSTRING CODE. EVER. AT ALL. It will make your grade very sad.
	 * ****************************************************************************************
	 */
	public String toString(){
		StringBuilder ret = new StringBuilder("Key\t:\tValue\t:\tHashcode\n");
		HashNode<K,V> curnode;
		for (int i = 0; i < table.length; i++){
			curnode = table[i];
			while (curnode != null){
				ret.append(curnode.getKey().toString() + "\t:\t" + curnode.getValue().toString() +  "\t:\t" + hashFunc.hash(curnode.getKey())%table.length + "\t\t\t");
				curnode = curnode.getNext();
			}
			if (table[i] != null){
				ret.append("\n");
			}
		}

		ret.append("\nSize: " + size() + "\nTable Length: " + table.length + "\nCollisions: " + collisionCount());
		return ret.toString();
	}

	public HashNode<K, V>[] getTable() {
		return table;
	}

	public void setTable(HashNode<K, V>[] table) {
		this.table = table;
	}

	public double getMaxLoad() {
		return maxLoad;
	}

	public void setMaxLoad(double maxLoad) {
		this.maxLoad = maxLoad;
	}

	public HashFunction<K> getHashFunc() {
		return hashFunc;
	}

	public void setHashFunc(HashFunction<K> hashFunc) {
		this.hashFunc = hashFunc;
	}

	public int size() {
		return size;
	}

	public void setSize(int size) {
		this.size = size;
	}

}
